﻿using System;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace Black.NBT
{
    public class NBTFile
    {
        #region Propriétés

        /// <summary>
        /// Obtient ou définit le nom du fichier
        /// </summary>
        public string Filename
        {
            get;
            set;
        }

        /// <summary>
        /// Obtient si le fichier est compressé
        /// </summary>
        public bool IsCompressed
        {
            get;
            private set;
        }

        /// <summary>
        /// Obtient la racine du fichier NBT
        /// </summary>
        public TagCompound Root
        {
            get;
            private set;
        }

        #endregion

        #region Constructeurs

        /// <summary>
        /// Constructeur de la classe
        /// </summary>
        /// <param name="filename">Chemin du fichier</param>
        /// <param name="compressed">TRUE compressé, FALSE sinon</param>
        private NBTFile(string filename, bool compressed)
        {
            // Initialisation

            this.Filename = filename;
            this.IsCompressed = compressed;

            // Chargement du fichier

            this.LoadFile();
        }

        #endregion

        #region Fonctions publiques

        /// <summary>
        /// Sauvegarde le fichier
        /// </summary>
        public void Save()
        {
            this.SaveFile(this.Filename, true);
        }

        /// <summary>
        /// Sauvegarde le fichier
        /// </summary>
        /// <param name="compressed">TRUE compresse les données, FALSE sinon</param>
        public void Save(bool compressed)
        {
            this.SaveFile(this.Filename, compressed);
        }

        /// <summary>
        /// Sauvegarde le fichier à un emplacement précis
        /// </summary>
        /// <param name="filename">Chemin du fichier cible</param>
        public void SaveAs(string filename)
        {
            this.SaveFile(filename, true);
        }

        /// <summary>
        /// Sauvegarde le fichier à un emplacement précis
        /// </summary>
        /// <param name="filename">Chemin du fichier cible</param>
        /// <param name="compressed">TRUE compresse les données, FALSE sinon</param>
        public void SaveAs(string filename, bool compressed)
        {
            this.SaveFile(filename, compressed);
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Charge le fichier
        /// </summary>
        private void LoadFile()
        {
            // Récupération du flux du fichier

            using (MemoryStream stream = this.ReadFile())
            {
                // On vérifie si le premier bit dans le fichier est ub tag de type Compound

                if (stream.ReadByte() == (int)TagType.Compound)
                    this.Root = TagCompound.Read(stream);
                else
                    throw new InvalidDataException("File format does not start with a TAG Compound.");
            }
        }

        /// <summary>
        /// Lit le contenu du fichier
        /// </summary>
        /// <returns>Stream de lecture</returns>
        private MemoryStream ReadFile()
        {
            MemoryStream finalStream = null;

            // Lecture du fichier brut

            using (FileStream fileStreamOriginal = File.OpenRead(this.Filename))
            {
                // Si compressé on le décompresse

                if (this.IsCompressed)
                {
                    using (GZipStream gzip = new GZipStream(fileStreamOriginal, CompressionMode.Decompress))
                    {
                        finalStream = new MemoryStream((int)fileStreamOriginal.Length);
                        gzip.CopyTo(finalStream);
                    }
                }
                else
                {
                    // Recopie des données du fichier en mémoire

                    finalStream = new MemoryStream((int)fileStreamOriginal.Length);
                    fileStreamOriginal.CopyTo(finalStream);
                }
            }

            // On remet la lecture du flux au début

            finalStream.Seek(0, SeekOrigin.Begin);

            // On renvoi le stream décompresser final

            return (finalStream);
        }

        /// <summary>
        /// Ecrit le fichier sur le disque
        /// </summary>
        /// <param name="filename">Chemin du fichier cible</param>
        /// <param name="compressed">TRUE compresse les données, FALSE sinon</param>
        private void SaveFile(string filename, bool compressed)
        {
            // Création du flux

            using (MemoryStream stream = new MemoryStream())
            {
                // Ecriture type root

                this.Root.WriteToStream(stream);

                // On se repositionne au début

                stream.Seek(0, SeekOrigin.Begin);

                // Compression du flux

                byte[] buffer;

                if (compressed)
                {
                    using (var compressStream = new MemoryStream())
                    {
                        using (var gzipStream = new GZipStream(compressStream, CompressionMode.Compress, true))
                        {
                            stream.CopyTo(gzipStream);
                        }

                        buffer = compressStream.ToArray();
                    }
                }
                else
                    buffer = stream.ToArray();

                // Ecriture dans le fichier

                File.WriteAllBytes(filename, buffer);
            }
        }

        #endregion

        #region Fonctions statiques publiques

        /// <summary>
        /// Charge un fichier NBT
        /// </summary>
        /// <param name="filename">Chemin d'accès du fichier</param>
        /// <returns>Instance du fichier</returns>
        public static NBTFile OpenFile(string filename)
        {
            return (NBTFile.OpenFile(filename, true));
        }

        /// <summary>
        /// Charge un fichier NBT
        /// </summary>
        /// <param name="filename">Chemin d'accès du fichier</param>
        /// <param name="isCompressed">TRUE est compressé, FALSE sinon</param>
        /// <returns>Instance du fichier</returns>
        public static NBTFile OpenFile(string filename, bool isCompressed)
        {

            // Vérification existance fichier

            if (!File.Exists(filename))
                throw (new ArgumentException(string.Format("The file '{0}' does not exist.", filename), "filename"));

            // Création de l'instance

            NBTFile file = new NBTFile(filename, isCompressed);

            // On retourne l'instance

            return (file);
        }

        #endregion

        #region Fonctions statiques interne

        /// <summary>
        /// Récupère un byte
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static byte ReadByte(MemoryStream stream)
        {
            byte[] buffer = new byte[1];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (buffer[0]);
        }

        /// <summary>
        /// Récupère un tableau de byte
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static byte[] ReadByteArray(MemoryStream stream)
        {
            // Récupération taille du tableau

            int length = NBTFile.ReadInt(stream);

            // Cration du buffer

            byte[] buffer = new byte[length];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On retourne la valeur

            return (buffer);
        }

        /// <summary>
        /// Récupère un double
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static double ReadDouble(MemoryStream stream)
        {
            byte[] buffer = new byte[8];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (BitConverter.ToDouble(buffer, 0));
        }

        /// <summary>
        /// Récupère un float
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static float ReadFloat(MemoryStream stream)
        {
            byte[] buffer = new byte[4];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (BitConverter.ToSingle(buffer, 0));
        }

        /// <summary>
        /// Récupère un int
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static int ReadInt(MemoryStream stream)
        {
            byte[] buffer = new byte[4];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (BitConverter.ToInt32(buffer, 0));
        }

        /// <summary>
        /// Récupère un long
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static long ReadLong(MemoryStream stream)
        {
            byte[] buffer = new byte[8];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (BitConverter.ToInt64(buffer, 0));
        }

        /// <summary>
        /// Récupère un short
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static short ReadShort(MemoryStream stream)
        {
            byte[] buffer = new byte[2];

            // Lecture du short

            stream.Read(buffer, 0, buffer.Length);

            // On inverse les bit l'ordre des bits est inversés

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            // On retourne la valeur

            return (BitConverter.ToInt16(buffer, 0));
        }

        /// <summary>
        /// Récupère un string
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <returns>Valeur lue</returns>
        internal static string ReadString(MemoryStream stream)
        {
            // Récupération de la taille de la chaîne

            int length = (int)NBTFile.ReadShort(stream);

            // Création du buffer

            byte[] buffer = new byte[length];

            // Lecture du flux

            stream.Read(buffer, 0, length);

            // On renvoi la valeur

            return (Encoding.UTF8.GetString(buffer));
        }

        /// <summary>
        /// Récupère un string
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="length">Taille de la chaine</param>
        /// <returns>Valeur lue</returns>
        internal static string ReadString(MemoryStream stream, int length)
        {
            byte[] buffer = new byte[length];

            // Lecture du flux

            stream.Read(buffer, 0, length);

            // On renvoi la valeur

            return (Encoding.UTF8.GetString(buffer));
        }

        /// <summary>
        /// Ecrit un byte dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteByte(MemoryStream stream, byte data)
        {
            stream.WriteByte(data);
        }

        /// <summary>
        /// Ecrit un tableau de byte dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteByteArray(MemoryStream stream, byte[] data)
        {
            NBTFile.WriteInt(stream, data.Length);
            stream.Write(data, 0, data.Length);
        }

        /// <summary>
        /// Ecrit un double dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteDouble(MemoryStream stream, double data)
        {
            byte[] buffer = BitConverter.GetBytes(data);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Ecrit un float dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteFloat(MemoryStream stream, float data)
        {
            byte[] buffer = BitConverter.GetBytes(data);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Ecrit un int dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteInt(MemoryStream stream, int data)
        {
            byte[] buffer = BitConverter.GetBytes(data);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Ecrit un long dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteLong(MemoryStream stream, long data)
        {
            byte[] buffer = BitConverter.GetBytes(data);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Ecrit un short dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteShort(MemoryStream stream, short data)
        {
            byte[] buffer = BitConverter.GetBytes(data);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(buffer);

            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Ecrit un string dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="data">Donnée a écrire</param>
        internal static void WriteString(MemoryStream stream, string data)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(data);

            NBTFile.WriteShort(stream, (short)data.Length);

            if (data.Length > 0)
                stream.Write(buffer, 0, buffer.Length);
        }

        #endregion
    }
}
